import importlib
import traceback
import time
from collections import defaultdict
from concurrent.futures import Future
from concurrent.futures import ThreadPoolExecutor
from copy import copy
from datetime import datetime, timedelta
from glob import glob
from pathlib import Path
from types import ModuleType
from typing import Any, Callable, Dict, List, Optional, Type

from vn_local.event import Event, EventEngine
from vn_local.trader.constant import (
    Direction,
    OrderType,
    Interval,
    Exchange,
    Offset,
    Status,
    Product
)
from vn_local.trader.database import DB_TZ
from vn_local.trader.engine import BaseEngine, MainEngine
from vn_local.trader.event import (
    EVENT_TICK,
    EVENT_ORDER,
    EVENT_TRADE,
    EVENT_ORDERTRADE,
    EVENT_CONTRACT,
    EVENT_PUBLIC_TRADE,
    EVENT_TIMER,
    EVENT_ACCOUNT
)
from vn_local.trader.object import (
    OrderRequest,
    SubscribeRequest,
    HistoryRequest,
    CancelRequest,
    CancelAllRequest,
    LogData,
    TickData,
    BarData,
    OrderData,
    TradeData,
    ContractData,
    OrderTradeData
)
from vn_local.trader.utility import load_json, save_json, extract_vt_symbol, round_to
from .base import (
    APP_NAME,
    EVENT_CTA_LOG,
    EVENT_CTA_STRATEGY,
    EVENT_CTA_STOPORDER,
    EngineType,
    StopOrder,
    StopOrderStatus,
    STOPORDER_PREFIX
)
from .template import CtaTemplate, TargetPosTemplate

# 停止单状态映射
STOP_STATUS_MAP: Dict[Status, StopOrderStatus] = {
    Status.SUBMITTING: StopOrderStatus.WAITING,
    Status.NOTTRADED: StopOrderStatus.WAITING,
    Status.PARTTRADED: StopOrderStatus.TRIGGERED,
    Status.ALLTRADED: StopOrderStatus.TRIGGERED,
    Status.CANCELLED: StopOrderStatus.CANCELLED,
    Status.REJECTED: StopOrderStatus.CANCELLED
}
from vn_local.trader.logger_util import VtLog


class CtaEngine(BaseEngine):
    """"""

    engine_type: EngineType = EngineType.LIVE  # live trading engine

    setting_filename: str = "mm_strategy_setting.json"
    data_filename: str = "mm_strategy_data.json"

    def __init__(self, main_engine: MainEngine, event_engine: EventEngine) -> None:
        """"""
        super().__init__(main_engine, event_engine, APP_NAME)

        self.strategy_setting: dict = {}  # strategy_name: dict
        self.strategy_data: dict = {}  # strategy_name: dict

        self.classes: dict = {}  # class_name: strategy_class
        self.strategies: dict = {}  # strategy_name: strategy

        self.symbol_strategy_map: defaultdict = defaultdict(list)  # vt_symbol: strategy list
        self.orderid_strategy_map: dict = {}  # vt_orderid: strategy
        self.strategy_orderid_map: defaultdict = defaultdict(set)  # strategy_name: orderid set

        self.stop_order_count: int = 0  # for generating stop_orderid
        self.stop_orders: Dict[str, StopOrder] = {}  # stop_orderid: stop_order

        self.init_executor: ThreadPoolExecutor = ThreadPoolExecutor(max_workers=1)

        self.vt_tradeids: set = set()  # for filtering duplicate trade

        self.tick_count = 0
        self.trade_count = 0

        # self.database: BaseDatabase = get_database()
        # self.datafeed: BaseDatafeed = get_datafeed()

    def init_engine(self) -> None:
        """"""
        # self.init_datafeed()
        self.load_strategy_class()
        self.load_strategy_setting()
        self.load_strategy_data()
        self.register_event()
        self.write_log(f"做市策略引擎初始化成功")

    def close(self) -> None:
        """"""
        self.stop_all_strategies()

    def if_stop_finished(self):
        finished = True
        for strategy in list(self.strategies.values()):
            strategy_orders = strategy.strategy_order_trades
            msg = (f"[on_engine_stop] {strategy.strategy_name}: 剩余未完成订单数量{len(strategy_orders)}, "
                   f"please check {strategy_orders}")
            self.write_log(msg)
            if strategy.strategy_order_trades:
                finished = False
                break
        return finished

    def register_event(self) -> None:
        """"""
        self.event_engine.register(EVENT_TIMER, self.process_timer_event)
        # self.event_engine.register(EVENT_CONTRACT, self.process_contract_event)
        self.event_engine.register(EVENT_PUBLIC_TRADE, self.process_public_trade_event)
        self.event_engine.register(EVENT_TICK, self.process_tick_event)
        self.event_engine.register(EVENT_ORDERTRADE, self.process_order_trade_event)
        self.event_engine.register(EVENT_ACCOUNT, self.process_account_event)
        self.event_engine.register(EVENT_CTA_STRATEGY, self.process_strategy_event)

    def process_timer_event(self, event: Event) -> None:
        self.trade_count += 1
        self.tick_count += 1
        # print(f"**********on timer event ********* trade_count={self.trade_count} tick_count={self.tick_count}")

        for strategy in list(self.strategies.values()):
            strategy.update_timer()

    def process_strategy_event(self, event: Event) -> None:
        data = event.data
        for strategy in list(self.strategies.values()):
            strategy.on_strategy_data(data)

    def process_account_event(self, event: Event) -> None:
        account = event.data
        for strategy in list(self.strategies.values()):
            strategy.on_account(account)

    # def init_datafeed(self) -> None:
    #     """
    #     Init datafeed client.
    #     """
    #     result: bool = self.datafeed.init(self.write_log)
    #     if result:
    #         self.write_log("数据服务初始化成功")
    #
    # def query_bar_from_datafeed(
    #     self, symbol: str, exchange: Exchange, interval: Interval, start: datetime, end: datetime
    # ) -> List[BarData]:
    #     """
    #     Query bar data from datafeed.
    #     """
    #     req: HistoryRequest = HistoryRequest(
    #         symbol=symbol,
    #         exchange=exchange,
    #         interval=interval,
    #         start=start,
    #         end=end
    #     )
    #     data: List[BarData] = self.datafeed.query_bar_history(req, self.write_log)
    #     return data

    def process_contract_event(self, event: Event) -> None:
        contract = event.data
        req = SubscribeRequest(contract.symbol, contract.exchange)

        self.main_engine.subscribe(req, contract.gateway_name)

    def process_public_trade_event(self, event: Event) -> None:
        public_trade = event.data

        if self.trade_count >= 100:
            msg = f"[on_mm_engine:on_public_trade]: symbol={public_trade.vt_symbol}, ts={public_trade.ts}, price={public_trade.price}"
            gateway = self.main_engine.get_gateway("BBC")
            gateway.write_log(msg, "bbc")

            self.trade_count = 0

        # print(f"engine")
        for strategy in self.strategies.values():
            if strategy.inited:
                self.call_strategy_func(strategy, strategy.on_public_trade, public_trade)
                # print("engine trade")

    def process_tick_event(self, event: Event) -> None:
        """"""
        tick: TickData = event.data
        # print("process_tick", tick)

        # strategies: list = self.symbol_strategy_map[tick.vt_symbol]
        # print("strategies", strategies)
        # if not strategies:
        #     return

        # self.check_stop_order(tick)
        if self.tick_count >= 100:
            msg = f"[on_mm_engine:on_tick]: symbol={tick.vt_symbol}, ts={tick.ts}, ask1={tick.ask_price_1}, bid1={tick.bid_price_1}"
            gateway = self.main_engine.get_gateway("BBC")
            gateway.write_log(msg, "bbc")
            self.tick_count = 0

        for strategy in self.strategies.values():
            if strategy.inited:
                self.call_strategy_func(strategy, strategy.on_tick, tick)

    def process_order_trade_event(self, event: Event) -> None:
        order_trade: OrderTradeData = event.data

        strategy: Optional[type] = self.orderid_strategy_map.get(order_trade.vt_orderid, None)
        if not strategy:
            return

        self.call_strategy_func(strategy, strategy.update_order_trade, order_trade)

    def send_order(
            self,
            strategy: CtaTemplate,
            contract: ContractData,
            price: float,
            volume: float,
            direction: Direction,
            order_type: OrderType,
            ref=""
    ) -> list:
        """
        """
        # start_time = time.time_ns()

        original_req = OrderRequest(
            symbol=contract.symbol,
            exchange=contract.exchange,
            direction=direction,
            offset=Offset.OPEN,
            type=order_type,
            price=price,
            volume=volume,
            reference=ref
        )

        req_list = [original_req]

        vt_orderids = []

        for req in req_list:
            # print('engine_send_order')
            vt_orderid = self.main_engine.send_order(
                req, contract.gateway_name)
            # print("vt_orderid:",vt_orderid)
            # Check if sending order successful
            if not vt_orderid:
                continue
            #
            vt_orderids.append(vt_orderid)

            # Save relationship between orderid and strategy.
            self.orderid_strategy_map[vt_orderid] = strategy
            self.strategy_orderid_map[strategy.strategy_name].add(vt_orderid)

            # 将委托号和主引擎的order_trades关系缓存
            orderid = vt_orderid.split(".")[1]
            order_trade = req.create_order_trade(orderid, gateway_name="BBC")
            # self.write_log(f"发出订单经过spreadEngine{vt_orderid}")
            self.main_engine.engines["oms"].order_trades[vt_orderid] = order_trade
        # end_time = time.time_ns()
        # msg = f"[on_ctaengine_send_order] {end_time-start_time}"
        # self.write_log(msg)
        return vt_orderids

    def amend_order(self):
        pass

    def cancel_order(self, strategy: CtaTemplate, vt_orderid: str) -> None:

        order = self.main_engine.get_order_trade(vt_orderid)
        if not order:
            self.write_log("撤单失败，找不到委托{}".format(vt_orderid), strategy)
            return

        req = order.create_cancel_request()
        self.main_engine.cancel_order(req, order.gateway_name)

    def cancel_all_orders(self, vt_symbol) -> None:
        symbol, exchange = extract_vt_symbol(vt_symbol)
        req = CancelAllRequest(exchange=exchange, symbol=symbol, product=Product.SPOT)
        self.main_engine.cancel_all_orders(req, gateway_name="BBC")

    def query_open_orders(self, req) -> None:
        self.main_engine.query_open_orders(req, gateway_name="BBC")

    def cancel_all(self, strategy: CtaTemplate) -> None:
        """
        Cancel all active orders of a strategy.
        """
        vt_orderids: set = self.strategy_orderid_map[strategy.strategy_name]
        if not vt_orderids:
            return

        for vt_orderid in copy(vt_orderids):
            self.cancel_order(strategy, vt_orderid)

    def get_engine_type(self) -> EngineType:
        """"""
        return self.engine_type

    def get_pricetick(self, strategy: CtaTemplate) -> float:
        """
        Return contract pricetick data.
        """
        contract: Optional[ContractData] = self.main_engine.get_contract(strategy.vt_symbol)

        if contract:
            return contract.pricetick
        else:
            return None

    def get_lot_size(self, strategy: CtaTemplate) -> float:
        contract: Optional[ContractData] = self.main_engine.get_contract(strategy.vt_symbol)

        if contract:
            return contract.min_volume
        else:
            return None

    def get_size(self, strategy: CtaTemplate) -> int:
        """
        Return contract size data.
        """
        contract: Optional[ContractData] = self.main_engine.get_contract(strategy.vt_symbol)

        if contract:
            return contract.size
        else:
            return None

    def load_bar(
            self,
            vt_symbol: str,
            days: int,
            interval: Interval,
            callback: Callable[[BarData], None],
            use_database: bool
    ) -> List[BarData]:
        """"""
        symbol, exchange = extract_vt_symbol(vt_symbol)
        end: datetime = datetime.now(DB_TZ)
        start: datetime = end - timedelta(days)
        bars: List[BarData] = []

        # Pass gateway and datafeed if use_database set to True
        if not use_database:
            # Query bars from gateway if available
            contract: Optional[ContractData] = self.main_engine.get_contract(vt_symbol)

            if contract and contract.history_data:
                req: HistoryRequest = HistoryRequest(
                    symbol=symbol,
                    exchange=exchange,
                    interval=interval,
                    start=start,
                    end=end
                )
                bars: List[BarData] = self.main_engine.query_history(req, contract.gateway_name)

            # Try to query bars from datafeed, if not found, load from database.
            else:
                bars: List[BarData] = self.query_bar_from_datafeed(symbol, exchange, interval, start, end)

        if not bars:
            bars: List[BarData] = self.database.load_bar_data(
                symbol=symbol,
                exchange=exchange,
                interval=interval,
                start=start,
                end=end,
            )

        return bars

    def load_tick(
            self,
            vt_symbol: str,
            days: int,
            callback: Callable[[TickData], None]
    ) -> List[TickData]:
        """"""
        symbol, exchange = extract_vt_symbol(vt_symbol)
        end: datetime = datetime.now(DB_TZ)
        start: datetime = end - timedelta(days)

        ticks: List[TickData] = self.database.load_tick_data(
            symbol=symbol,
            exchange=exchange,
            start=start,
            end=end,
        )

        return ticks

    def call_strategy_func(
            self, strategy: CtaTemplate, func: Callable, params: Any = None
    ) -> None:
        """
        Call function of a strategy and catch any exception raised.
        """
        try:
            if params:
                func(params)
            else:
                func()
        except Exception:
            strategy.trading = False
            strategy.inited = False

            msg: str = f"触发异常已停止\n{traceback.format_exc()}"
            self.write_log(msg, strategy)

    def add_strategy(
            self, class_name: str, strategy_name: str, vt_symbol: str, setting: dict
    ) -> None:

        """
        Add a new strategy.
        """
        if strategy_name in self.strategies:

            self.write_log(f"创建策略失败，存在重名{strategy_name}")
            return
        strategy_class: Optional[Type[CtaTemplate]] = self.classes.get(class_name, None)
        if not strategy_class:

            self.write_log(f"创建策略失败，找不到策略类{class_name}")
            return

        if "." not in vt_symbol:

            self.write_log("创建策略失败，本地代码缺失交易所后缀")
            return

        _, exchange_str = vt_symbol.split(".")
        if exchange_str not in Exchange.__members__:
            self.write_log("创建策略失败，本地代码的交易所后缀不正确")
            return

        strategy: CtaTemplate = strategy_class(self, strategy_name, vt_symbol, setting)
        self.strategies[strategy_name] = strategy

        # Add vt_symbol to strategy map.
        strategies: list = self.symbol_strategy_map[vt_symbol]
        strategies.append(strategy)

        # Update to setting file.
        self.update_strategy_setting(strategy_name, setting)

        self.put_strategy_event(strategy)

    def init_strategy(self, strategy_name: str) -> Future:
        """
        Init a strategy.
        """
        return self.init_executor.submit(self._init_strategy, strategy_name)

    def _init_strategy(self, strategy_name: str) -> None:
        """
        Init strategies in queue.
        """
        strategy: CtaTemplate = self.strategies[strategy_name]

        if strategy.inited:
            self.write_log(f"{strategy_name}已经完成初始化，禁止重复操作")
            return

        self.write_log(f"{strategy_name}开始执行初始化")

        # Call on_init function of strategy
        self.call_strategy_func(strategy, strategy.on_init)

        # Restore strategy data(variables)
        data: Optional[dict] = self.strategy_data.get(strategy_name, None)
        if data:
            for name in strategy.variables:
                value = data.get(name, None)
                if value is not None:
                    setattr(strategy, name, value)

        # Subscribe market data
        contract: Optional[ContractData] = self.main_engine.get_contract(strategy.vt_symbol)
        if contract:
            req: SubscribeRequest = SubscribeRequest(
                symbol=contract.symbol, exchange=contract.exchange)
            self.main_engine.subscribe(req, contract.gateway_name)
        else:
            self.write_log(f"行情订阅失败，找不到合约{strategy.vt_symbol}", strategy)

        # Put event to update init completed status.
        strategy.inited = True
        self.put_strategy_event(strategy)
        self.write_log(f"{strategy_name}初始化完成")

    def start_strategy(self, strategy_name: str) -> None:
        """
        Start a strategy.
        """
        strategy: CtaTemplate = self.strategies[strategy_name]
        if not strategy.inited:
            self.write_log(f"策略{strategy.strategy_name}启动失败，请先初始化")
            return

        if strategy.trading:
            self.write_log(f"{strategy_name}已经启动，请勿重复操作")
            return

        self.call_strategy_func(strategy, strategy.on_start)
        strategy.trading = True

        self.put_strategy_event(strategy)

    def stop_strategy(self, strategy_name: str) -> None:
        """
        Stop a strategy.
        """
        strategy: CtaTemplate = self.strategies[strategy_name]
        if not strategy.trading:
            return

        # Call on_stop function of the strategy
        self.call_strategy_func(strategy, strategy.on_stop)

        # Change trading status of strategy to False
        strategy.trading = False

        # Cancel all orders of the strategy
        # self.cancel_all(strategy)

        # Sync strategy variables to data file
        self.sync_strategy_data(strategy)

        # Update GUI
        self.put_strategy_event(strategy)

    def edit_strategy(self, strategy_name: str, setting: dict) -> None:
        """
        Edit parameters of a strategy.
        """
        strategy: CtaTemplate = self.strategies[strategy_name]
        strategy.update_setting(setting)

        # self.update_strategy_setting(strategy_name, setting)
        # self.put_strategy_event(strategy)

    def remove_strategy(self, strategy_name: str) -> bool:
        """
        Remove a strategy.
        """
        strategy: CtaTemplate = self.strategies[strategy_name]
        if strategy.trading:
            self.write_log(f"策略{strategy.strategy_name}移除失败，请先停止")
            return

        # Remove setting
        self.remove_strategy_setting(strategy_name)

        # Remove from symbol strategy map
        strategies: list = self.symbol_strategy_map[strategy.vt_symbol]
        strategies.remove(strategy)

        # Remove from active orderid map
        if strategy_name in self.strategy_orderid_map:
            vt_orderids: set = self.strategy_orderid_map.pop(strategy_name)

            # Remove vt_orderid strategy map
            for vt_orderid in vt_orderids:
                if vt_orderid in self.orderid_strategy_map:
                    self.orderid_strategy_map.pop(vt_orderid)

        # Remove from strategies
        self.strategies.pop(strategy_name)

        self.write_log(f"策略{strategy.strategy_name}移除成功")
        return True

    def load_strategy_class(self) -> None:
        """
        Load strategy class from source code.
        """
        path1: Path = Path(__file__).parent.joinpath("strategies")
        self.load_strategy_class_from_folder(path1, "vn_local.app.market_maker.strategies")

        path2: Path = Path.cwd().joinpath("strategies")
        self.load_strategy_class_from_folder(path2, "strategies")

    def load_strategy_class_from_folder(self, path: Path, module_name: str = "") -> None:
        """
        Load strategy class from certain folder.
        """
        for suffix in ["py", "pyd", "so"]:
            pathname: str = str(path.joinpath(f"*.{suffix}"))
            for filepath in glob(pathname):
                filename = Path(filepath).stem
                name: str = f"{module_name}.{filename}"
                # print(name)
                self.load_strategy_class_from_module(name)

    def load_strategy_class_from_module(self, module_name: str) -> None:
        """
        Load strategy class from module file.
        """
        try:
            module: ModuleType = importlib.import_module(module_name)
            # 重载模块，确保如果策略文件中有任何修改，能够立即生效。
            importlib.reload(module)
            for name in dir(module):
                value = getattr(module, name)
                # print(f'a = {isinstance(value, type)}')
                # print(f'b = {issubclass(value, CtaTemplate)}, {value}')
                # print(f'c = {value not in {CtaTemplate, TargetPosTemplate}}')
                if (
                        isinstance(value, type)
                        and issubclass(value, CtaTemplate)
                        and value not in {CtaTemplate, TargetPosTemplate}
                ):
                    self.classes[value.__name__] = value
                    # print(2)
        except Exception as e:  # noqa
            msg: str = f"策略文件{module_name}加载失败，触发异常：\n{traceback.format_exc()}"
            # print(msg)
            self.write_log(msg)

    def load_strategy_data(self) -> None:
        """
        Load strategy data from json file.
        """
        self.strategy_data = load_json(self.data_filename)

    def sync_strategy_data(self, strategy: CtaTemplate) -> None:
        """
        Sync strategy data into json file.
        """
        data: dict = strategy.get_variables()
        data.pop("inited")  # Strategy status (inited, trading) should not be synced.
        data.pop("trading")

        self.strategy_data[strategy.strategy_name] = data
        save_json(self.data_filename, self.strategy_data)

    def get_all_strategy_class_names(self) -> list:
        """
        Return names of strategy classes loaded.
        """
        return list(self.classes.keys())

    def get_strategy_class_parameters(self, class_name: str) -> dict:
        """
        Get default parameters of a strategy class.
        """
        strategy_class: Type[CtaTemplate] = self.classes[class_name]

        parameters: dict = {}
        for name in strategy_class.parameters:
            parameters[name] = getattr(strategy_class, name)

        return parameters

    def get_strategy_parameters(self, strategy_name) -> dict:
        """
        Get parameters of a strategy.
        """
        strategy: CtaTemplate = self.strategies[strategy_name]
        return strategy.get_parameters()

    def init_all_strategies(self) -> Dict[str, Future]:
        """
        """
        futures: Dict[str, Future] = {}
        for strategy_name in self.strategies.keys():
            futures[strategy_name] = self.init_strategy(strategy_name)
        return futures

    def start_all_strategies(self) -> None:
        """
        """
        for strategy_name in self.strategies.keys():
            self.start_strategy(strategy_name)

    def stop_all_strategies(self) -> None:
        """
        """
        for strategy_name in self.strategies.keys():
            self.stop_strategy(strategy_name)

    def load_strategy_setting(self) -> None:
        """
        Load setting file.
        """
        self.strategy_setting = load_json(self.setting_filename)

        for strategy_name, strategy_config in self.strategy_setting.items():

            self.add_strategy(
                strategy_config["class_name"],
                strategy_name,
                strategy_config["vt_symbol"],
                strategy_config["setting"]
            )

    def get_strategy_setting_from_file(self, strategy: CtaTemplate) -> Dict:
        self.strategy_setting = load_json(self.setting_filename)
        for strategy_name, strategy_config in self.strategy_setting.items():
            if strategy_name != strategy.strategy_name:
                continue
            return strategy_config["setting"]

    def update_strategy_setting(self, strategy_name: str, setting: dict) -> None:
        """
        Update setting file.
        """
        strategy: CtaTemplate = self.strategies[strategy_name]

        self.strategy_setting[strategy_name] = {
            "class_name": strategy.__class__.__name__,
            "vt_symbol": strategy.vt_symbol,
            "setting": setting,
        }
        save_json(self.setting_filename, self.strategy_setting)

    def remove_strategy_setting(self, strategy_name: str) -> None:
        """
        Update setting file.
        """
        if strategy_name not in self.strategy_setting:
            return

        self.strategy_setting.pop(strategy_name)
        save_json(self.setting_filename, self.strategy_setting)

        self.strategy_data.pop(strategy_name, None)
        save_json(self.data_filename, self.strategy_data)

    def put_stop_order_event(self, stop_order: StopOrder) -> None:
        """
        Put an event to update stop order status.
        """
        event: Event = Event(EVENT_CTA_STOPORDER, stop_order)
        self.event_engine.put(event)

    def put_strategy_event(self, strategy: CtaTemplate) -> None:
        """
        Put an event to update strategy status.
        """
        data: dict = strategy.get_data()
        event: Event = Event(EVENT_CTA_STRATEGY, data)
        # print("put_strategy_event", data)
        self.event_engine.put(event)

    def write_log(self, msg: str, strategy: CtaTemplate = None) -> None:
        """
        Create market_maker engine log event.
        """
        if strategy:
            msg: str = f"[{strategy.strategy_name}]  {msg}"

        log: LogData = LogData(msg=msg, gateway_name=APP_NAME, prefix="vt")
        event: Event = Event(type=EVENT_CTA_LOG, data=log)
        self.event_engine.put(event)

    def send_email(self, msg: str, strategy: CtaTemplate = None) -> None:
        """
        Send email to default receiver.
        """
        if strategy:
            subject: str = f"{strategy.strategy_name}"
        else:
            subject: str = "CTA策略引擎"

        self.main_engine.send_email(subject, msg)
